%%-------------------------------------------------------------------
%% @author
%%     ChicagoBoss Team and contributors, see AUTHORS file in root directory
%% @end
%% @copyright
%%     This file is part of ChicagoBoss project.
%%     See AUTHORS file in root directory
%%     for license information, see LICENSE file in root directory
%% @end
%% @doc
%%-------------------------------------------------------------------

-module(boss_web_controller).
-behaviour(gen_server).

-export([start_link/0, start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([merge_headers/2]).
-export([handle_news_for_cache/3]).
-export([run_init_scripts/1, generate_session_id/1]).

-export([execute_action/4, filter_config/1, filters_for_function/1]).

-include("boss_web.hrl").

start_link() ->
    init_master_node().

init_master_node() -> start_link([]).

start_link(Config) ->
    gen_server:start_link({local, boss_web}, ?MODULE, Config, []).


terminate(Reason, #state{ is_master_node = true } = State) ->
    boss_news:stop(),
    boss_mq:stop(),
    boss_session:stop(),
    stop_smtp(),
    terminate(Reason, State#state{ is_master_node = false });

terminate(_Reason, #state{router_adapter=RouterAdapter}=State) ->
    lists:map(fun(AppInfo) ->
                stop_init_scripts(AppInfo#boss_app_info.application, AppInfo#boss_app_info.init_data)
        end, State#state.applications),
    Services = [ boss_translator, RouterAdapter, boss_model_manager, boss_cache],
    [Service:stop() ||Service <- Services],
    application:stop(simple_bridge),
    application:stop(elixir).


terminate_smtp(Pid) when is_pid(Pid) ->
    gen_smtp_server:stop(Pid);
terminate_smtp(_) ->
    ok.

stop_smtp() ->
    case boss_env:get_env(smtp_server_enable, false) of
        true ->
            SMTPPid = global:whereis_name(boss_smtp_server),
        terminate_smtp(SMTPPid);
        _ ->
            ok
    end.

init(Config) ->
    ThisNode                   = erlang:node(),
    Env                        = boss_web_controller_init:init_services(),
    {ok,MasterNode}            = boss_web_controller_init:init_master_node(Env, ThisNode),

    boss_web_controller_init:init_mail_service(),
    RouterAdapter              = boss_env:router_adapter(),

    {SSLEnable, SSLOptions}    = boss_web_controller_init:init_ssl(),
    ServicesSupPid             = boss_web_controller_init:init_master_services(ThisNode, MasterNode),
    ServerConfig               = init_server_config(Config, RouterAdapter),
    Pid                        = boss_web_controller_init:init_webserver(
                                   ThisNode, MasterNode, SSLEnable,
                                   SSLOptions, ServicesSupPid, ServerConfig),
    {ok, #state{
                router_adapter  = RouterAdapter,
                service_sup_pid = ServicesSupPid,
                http_pid        = Pid,
                is_master_node  = (ThisNode =:= MasterNode) }, 0}.

init_server_config(Config, RouterAdapter) ->
    [{loop, fun(Req) ->
            boss_web_controller_handle_request:handle_request(Req, RouterAdapter)
            end} | Config].

handle_info(timeout, #state{service_sup_pid = ServicesSupPid} = State) ->
    Applications    = boss_env:get_env(applications, []),
    AppInfoList     = boss_web_controller_util:start_boss_applications(Applications,
                                                                       ServicesSupPid,
                                                                       State),
    {noreply, State#state{ applications = AppInfoList }}.



handle_call({reload_translation, Locale}, _From, State) ->
    _ = lists:map(fun(AppInfo) ->
                [{_, TranslatorPid, _, _}] = supervisor:which_children(AppInfo#boss_app_info.translator_sup_pid),
                boss_translator:reload(TranslatorPid, Locale)
        end, State#state.applications),
    {reply, ok, State};
handle_call(reload_all_translations, _From, State) ->
    _ = lists:map(fun(AppInfo) ->
                [{_, TranslatorPid, _, _}] = supervisor:which_children(AppInfo#boss_app_info.translator_sup_pid),
                boss_translator:reload_all(TranslatorPid)
        end, State#state.applications),
    {reply, ok, State};
handle_call(reload_routes, _From, #state{router_adapter=RouterAdapter}=State) ->
    _ = lists:map(fun(AppInfo) ->
                [{_, RouterPid, _, _}] = supervisor:which_children(AppInfo#boss_app_info.router_sup_pid),
                RouterAdapter:reload(RouterPid)
        end, State#state.applications),
    {reply, ok, State};
handle_call(reload_init_scripts, _From, State) ->
    NewApplications = lists:map(fun(AppInfo) ->
                stop_init_scripts(AppInfo#boss_app_info.application, AppInfo#boss_app_info.init_data),
                NewInitData = run_init_scripts(AppInfo#boss_app_info.application),
                AppInfo#boss_app_info{ init_data = NewInitData }
        end, State#state.applications),
    {reply, ok, State#state{ applications = NewApplications }};
handle_call(get_all_routes, _From, #state{router_adapter=RouterAdapter}=State) ->
    Routes = lists:map(fun(AppInfo) ->
                [{_, RouterPid, _, _}] = supervisor:which_children(AppInfo#boss_app_info.router_sup_pid),
                {AppInfo#boss_app_info.application, RouterAdapter:get_all(RouterPid)}
        end, State#state.applications),
    {reply, Routes, State};
handle_call(get_all_models, _From, State) ->
    Models = lists:foldl(fun(AppInfo, Acc) ->
                boss_files:model_list(AppInfo#boss_app_info.application) ++ Acc
        end, [], State#state.applications),
    {reply, Models, State};
handle_call(get_all_applications, _From, State) ->
    Applications = lists:map(fun(AppInfo) -> AppInfo#boss_app_info.application end,
                             State#state.applications),
    {reply, Applications, State};
handle_call({translator_pid, App}, _From, State) ->
    Pid = lists:foldl(fun
            (#boss_app_info{ application = App1, translator_sup_pid = SupPid }, _) when App1 =:= App ->
                [{_, TranslatorPid, _, _}] = supervisor:which_children(SupPid),
                TranslatorPid;
            (_, Res) ->
                Res
        end, undefined, State#state.applications),
    {reply, Pid, State};
handle_call({router_pid, App}, _From, State) ->
    Pid = lists:foldl(fun
            (#boss_app_info{ application = App1, router_sup_pid = SupPid }, _) when App1 =:= App ->
                [{_, RouterPid, _, _}] = supervisor:which_children(SupPid),
                RouterPid;
            (_, Res) ->
                Res
        end, undefined, State#state.applications),
    {reply, Pid, State};
handle_call({application_info, App}, _From, State) ->
    AppInfo = lists:keyfind(App, 2, State#state.applications),
    {reply, AppInfo, State};
handle_call({base_url, App}, _From, State) ->
    {_,AppInfo,_ } = handle_call({application_info, App},_From, State),
    BaseURL        = AppInfo#boss_app_info.base_url,
    {reply, BaseURL, State};
handle_call({static_prefix, App}, _From, State) ->
    {_,AppInfo,_ } = handle_call({application_info, App},_From, State),
    StaticPrefix        = AppInfo#boss_app_info.static_prefix,
    {reply, StaticPrefix, State};
handle_call({domains, App}, _From, State) ->
    {_,AppInfo,_ } = handle_call({application_info, App},_From, State),
    DomainList        = AppInfo#boss_app_info.domains,
    {reply, DomainList, State}.

handle_cast(_Request, State) ->
    {noreply, State}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

stop_init_scripts(Application, InitData) ->
    lists:foldr(fun(File, _) ->
                case boss_compiler:compile(File, [{include_dirs, [boss_files_util:include_dir() | boss_env:get_env(boss, include_dirs, [])]}]) of
                    {ok, Module} ->
                        case proplists:get_value(Module, InitData, init_failed) of
                           init_failed ->
                               ok;
                           ScriptInitData ->
                               catch Module:stop(ScriptInitData)
                       end;
                   Error -> lager:error("Compilation of ~p failed: ~p", [File, Error])
                end
        end, ok, boss_files:init_file_list(Application)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
run_init_scripts(AppName) ->
    lists:foldl(fun(File, Acc) ->
            CompileResult = boss_compiler:compile(File, [{include_dirs, [boss_files_util:include_dir() | boss_env:get_env(boss, include_dirs, [])]}]),
            process_compile_result(File, Acc, CompileResult)
        end, [], boss_files:init_file_list(AppName)).

process_compile_result(File, Acc, {ok, Module}) ->
    InitResult =  Module:init(),
    init_result(File, Acc, Module, InitResult);
process_compile_result(File, Acc, Error) ->
    _ = lager:error("Compilation of ~p failed: ~p", [File, Error]),
    Acc.

init_result(_File, Acc, Module, {ok, Info}) ->
    [{Module, Info}|Acc];
init_result(_File, Acc, Module, ok) ->
    [{Module, true}|Acc];
init_result(File, Acc, _Module,Error) ->
    _ = lager:error("Execution of ~p failed: ~p", [File, Error]),
    Acc.

generate_session_id(Request) ->
    case boss_env:get_env(session_enable, true) of
        true ->
            SessionKey = boss_session:get_session_key(),
            boss_session:new_session(Request:cookie(SessionKey));
        false ->
            undefined
    end.


execute_action({Controller, Action}, AppInfo, RequestContext, LocationTrail) ->
    execute_action({Controller, Action, []}, AppInfo, RequestContext, LocationTrail);

execute_action({Controller, Action, Tokens}, AppInfo, RequestContext, LocationTrail) when is_atom(Action) ->
    execute_action({Controller, atom_to_list(Action), Tokens}, AppInfo, RequestContext, LocationTrail);

execute_action({Controller, Action, Tokens} = Location, AppInfo, RequestContext, LocationTrail) ->
    Req             = proplists:get_value(request, RequestContext),
    SessionID       = proplists:get_value(session_id, RequestContext),
    IsMemberOfList = lists:member(Location, LocationTrail),
    execute_action_check_for_circular_redirect(Controller, Action, Tokens,
                                               Location, AppInfo,
                                               RequestContext, LocationTrail,
                                               Req, SessionID, IsMemberOfList).

execute_action_check_for_circular_redirect(_Controller, _Action, _Tokens,
                                           _Location, _AppInfo, _RequestContext,
                                           _LocationTrail, _Req, SessionID,
                                           true) ->
    {{error, "Circular redirect!"}, SessionID};
execute_action_check_for_circular_redirect(Controller, Action, Tokens,
                                           Location, AppInfo, RequestContext,
                                           LocationTrail, Req, SessionID,
                                           false) ->
    execute_action_inner(Controller, Action, Tokens, Location, AppInfo,
             RequestContext, LocationTrail, Req, SessionID).


execute_action_inner(Controller, Action, Tokens, Location, AppInfo,
                     RequestContext, LocationTrail, Req, SessionID) ->
    % do not convert a list to an atom until we are sure the controller/action
    % pair exists. this prevents a memory leak due to atom creation.
    Adapters                            = [boss_controller_adapter_pmod,
                                           boss_controller_adapter_elixir],

    Adapter                             = make_action_adapter(Controller, AppInfo, Adapters),
    SessionID1                          = make_action_session_id(Controller, AppInfo, Req,
                                                                 SessionID, Adapter),
    RequestMethod                       = Req:request_method(),
    RequestContext1                     = [{request, Req},
                                           {session_id, SessionID1},
                                           {method, RequestMethod},
                                           {action, Action},
                                           {tokens, Tokens}],
    AdapterInfo                         = Adapter:init(AppInfo#boss_app_info.application,
                                                       Controller,
                                                       AppInfo#boss_app_info.controller_modules,
                                                       RequestContext1),

    RequestContext2                     = [{controller_module, element(1, AdapterInfo)}|RequestContext1],
    {ActionResult, RequestContext3}     = apply_action(Req, Adapter,
                                                       AdapterInfo,
                                                       RequestContext2),
    RenderedResult                      = boss_web_controller_render:render_result(Location, AppInfo, RequestContext,
                                                                                   LocationTrail, Adapter, AdapterInfo,
                                                                                   ActionResult, RequestContext3),
    FinalResult                         = apply_after_filters(Adapter,
                                                              AdapterInfo,
                                                              RequestContext3,
                                                              RenderedResult),
    {FinalResult, SessionID1}.

apply_action(Req, Adapter, AdapterInfo, RequestContext2) ->
    case apply_before_filters(Adapter, AdapterInfo, RequestContext2) of
    {ok, RC3} ->
        EffectiveRequestMethod = case Req:request_method() of
                     'HEAD' -> 'GET';
                     Method -> Method
                     end,

            RequestContext = lists:keyreplace(method, 1, RC3, {method, EffectiveRequestMethod}),
        {call_controller_action(Adapter, AdapterInfo, RequestContext),
         RC3};
    {not_ok, RC3, NotOK} ->
        {NotOK, RC3}
    end.

call_controller_action(Adapter, AdapterInfo, RequestContext) ->
    try
        Adapter:action(AdapterInfo, RequestContext)
    catch
        Class:Error ->
            _ = lager:error("Error in controller ~s", [boss_log_util:stacktrace(Class, Error)]),
            {error, "Error in controller, see console.log for details\n"}
    end.

make_action_session_id(Controller, AppInfo, Req, undefined, Adapter) ->
    case Adapter:wants_session(AppInfo#boss_app_info.application,
                   Controller,
                   AppInfo#boss_app_info.controller_modules) of
    true -> generate_session_id(Req);
    _    -> undefined
    end;
make_action_session_id(_Controller, _AppInfo, _Req, SessionID, _Adapter) ->
    SessionID.


make_action_adapter(Controller, AppInfo, Adapters) ->
    lists:foldl(fun
            (A, false) ->
            case A:accept(AppInfo#boss_app_info.application, Controller,
                      AppInfo#boss_app_info.controller_modules) of
                true -> A;
                _ -> false
            end;
            (_, Acc) -> Acc
                end, false, Adapters).

process_before_filter_result({ok, Context}, _) -> {ok, Context};
process_before_filter_result(NotOK, Context) -> {not_ok, Context, NotOK}.

filter_config(Filter) ->
    FilterKey = case proplists:get_value('config_key', Filter:module_info(exports)) of
        0 -> Filter:config_key();
        _ -> Filter
    end,
    FilterValue = case proplists:get_value(FilterKey, boss_env:get_env(controller_filter_config, [])) of
        undefined ->
            case proplists:get_value('config_default_value', Filter:module_info(exports)) of
                0 -> Filter:config_default_value();
                _ -> undefined
            end;
        Value -> Value
    end,
    {FilterKey, FilterValue}.

apply_before_filters(Adapter, AdapterInfo, RequestContext) ->
    % legacy API
    case Adapter:before_filter(AdapterInfo, RequestContext) of
        {ok, RequestContext1} ->
            % new API
            GlobalFilters = filters_for_function('before_filter'),
            ActionFilters = Adapter:filters('before', AdapterInfo, RequestContext, GlobalFilters),
            lists:foldl(fun
                    (Filter, {ok, Context}) when is_atom(Filter) ->
                        {FilterKey, DefaultConfig} = filter_config(Filter),
                        FilterConfig = Adapter:filter_config(AdapterInfo, FilterKey, DefaultConfig, RequestContext),
                        FilterResult = case proplists:get_value(before_filter, Filter:module_info(exports)) of
                            2 -> Filter:before_filter(FilterConfig, Context);
                            _ -> {ok, Context}
                        end,
                        process_before_filter_result(FilterResult, Context);
                    (_Filter, NotOK) -> NotOK
                end, {ok, RequestContext1}, ActionFilters);
        Other ->
            {not_ok, RequestContext, Other}
    end.

apply_after_filters(Adapter, AdapterInfo, RequestContext, RenderedResult) ->
    GlobalFilters = filters_for_function('after_filter'),
    ActionFilters = Adapter:filters('after', AdapterInfo, RequestContext, GlobalFilters),

    % legacy API
    RenderedResult1 = Adapter:after_filter(AdapterInfo, RequestContext, RenderedResult),

    % new API
    lists:foldl(fun
            (Filter, Rendered) when is_atom(Filter) ->
                {FilterKey, DefaultConfig} = filter_config(Filter),
                FilterConfig = Adapter:filter_config(AdapterInfo, FilterKey, DefaultConfig, RequestContext),
                case proplists:get_value(after_filter, Filter:module_info(exports)) of
                    3 -> Filter:after_filter(Rendered, FilterConfig, RequestContext);
                    _ -> Rendered
                end
        end, RenderedResult1, ActionFilters).

handle_news_for_cache(_, _, {Prefix, Key}) ->
    boss_cache:delete(Prefix, Key),
    {ok, cancel_watch}.

% merges headers with preference on Headers1.
merge_headers(Headers1, Headers2) ->
    simple_bridge_util:ensure_headers(Headers1, Headers2).

filters_for_function(Function) ->
    lists:foldr(fun(Module, List) ->
                Exports = Module:module_info(exports),
                case proplists:get_value(Function, Exports) of
                    undefined -> List;
                    _ -> [Module|List]
                end
        end, [], ?BUILTIN_CONTROLLER_FILTERS ++ boss_env:get_env(controller_filter_modules, [])).
